延續前幾天完成的會員登入、預約掛號 API、個人資料及我的預約功能,今天開始進行智慧醫院管理平台第一階段「醫院預約網站」的完整系統整合測試。
Day 26 已經完成會員註冊與登入,Day 27 完成 Oracle 預約資料表及 ASP.NET Core 預約 API,Day 28 則將個人資料、我的預約及預約詳細頁面串接正式 API。
不過,在準備測試整個預約流程時,我發現原本的「預約掛號」與「預約成功」頁面仍然使用固定的 Demo 資料。
也就是說,雖然後端已經可以建立正式預約,但使用者還無法完整地透過 Angular 網站完成掛號。
因此,今天除了測試前後端整合,也需要先補齊正式預約頁面的 API 串接,再從實際使用者的角度,檢查整個網站的操作流程。
開始 Day 29 前,網站已經有以下功能:
| 功能 | 完成情況 |
|---|---|
| 醫師查詢 | 已串接正式醫師 API |
| 醫師詳細資訊 | 已串接正式醫師 API |
| 會員註冊與登入 | 已完成 |
| 個人資料查看及編輯 | 已串接 Profile API |
| 我的預約列表 | 已串接正式預約 API |
| 預約詳細及取消 | 已串接正式預約 API |
| 預約掛號頁面 | 仍使用 Demo 資料 |
| 預約成功頁面 | 仍使用 Demo 資料 |
原本我採取先完成 HTML、CSS 與 Angular 畫面的開發方式,因此早期的預約頁面使用固定醫師、日期及病人資訊進行展示。
但開始整合時,這些固定資料已經無法滿足正式掛號需求。
這次首先發現一個問題:原本 Demo 醫師使用 d1、d2 等識別碼,而 Oracle 正式醫師使用數字 ID,例如 1、2。
兩者並不是相同的資料來源,因此不能直接將 d1 轉換為 1。
另外,正式醫師詳細頁原本還沒有「立即預約」入口,只有 Demo 醫師詳細頁能進入掛號頁。
因此,這次決定保留原本的 Demo 流程,另外建立正式預約路由,避免修改後影響之前完成的展示頁面。
這次規劃的路由如下:
| 正式頁面 | Demo 頁面 |
|---|---|
/doctors/1 |
/doctors/d1 |
/reserve/1 |
/booking/d1 |
/reservation-success/4 |
/booking-success/d1 |
/appointments |
/demo/appointments |
/appointments/4 |
/demo/appointments/:id |
正式頁面透過 HTTP API 取得 Oracle 資料,而 Demo 頁面繼續使用原本的固定資料。
這樣不只方便測試,也能避免正式 API 發生錯誤時,畫面卻顯示 Demo 資料,讓使用者誤以為掛號已經完成。
確認現有結構後,我開始建立正式預約掛號頁面。
主要新增:
src/app/pages/
├── reservation.ts
├── reservation.html
└── reservation.css
並新增日期化診次 API 所需的 Service 與 Models:
src/app/core/
├── booking-api.models.ts
├── booking-api.service.ts
└── booking-validation.ts
正式路由設定為:
/reserve/:doctorId
其中 doctorId 必須是合法的正式數字 ID,不接受 Demo 的 d1 等識別碼。
另外,也使用既有的 authGuard 保護正式預約頁。
如果會員尚未登入,會先導向登入頁面,登入後再返回原本要進入的掛號頁。
這次沒有重新設計整個預約頁,而是沿用原本已完成的 HTML、CSS 與 RWD。
預約表單仍保留四個主要區域:
主要調整資料來源與互動功能,讓原本的展示介面可以真正取得後端資料。
另外,正式頁面的會員姓名使用登入後的資料,不再顯示 Demo 病人的固定姓名、生日或身分證字號。
正式醫師詳細頁與「立即預約」按鈕

正式預約掛號頁面

在 Day 27 設計資料庫時,我已經將每週門診時間與實際可預約診次分開。
因此,Day 29 正式掛號頁不能直接使用原本的每週門診時間表,而是必須查詢指定日期的正式診次。
使用的 API:
GET /api/schedules?doctorId=1&date=2026-11-02
API 回傳的資料包括:
其中,正式預約使用的 scheduleId,就是 API 回傳的 data[].id。
不能使用日期文字或時間字串代替。
這次讓預約頁依照 API 的實際結果顯示診次。
| 診次情況 | 前端處理 |
|---|---|
| 可以預約 | 顯示為可選取 |
| 已額滿 | 顯示名額已滿,不能選取 |
| 未開放 | 顯示尚未開放 |
| 已開始或過期 | 不允許選取 |
| 當天沒有診次 | 顯示沒有門診 |
| API 查詢失敗 | 顯示錯誤訊息及重新查詢 |
另外,日期與時間統一以 Asia/Taipei 顯示,避免時區轉換造成預約日期錯誤。
串接過程中也需要處理非同步請求。
例如使用者先選擇 11 月 2 日,又快速切換到 11 月 3 日,前一個日期的 API 回應可能比較晚才抵達。
如果沒有處理,畫面就可能顯示錯誤日期的診次。
因此,這次使用 RxJS 的 switchMap 處理日期查詢,當日期改變時取消前一次查詢,並清除原本選擇的診次。
這樣可以避免較晚回來的舊資料覆蓋目前的畫面。
正式日期化診次、剩餘名額與預約狀態

完成診次查詢後,接著將「確認預約」按鈕串接至 Day 27 的正式預約 API。
使用的 API:
POST /api/appointments
Request 範例:
{
"scheduleId": 2,
"visitReason": "虛構掛號流程整合測試"
}
這次前端只需要傳送兩個欄位:
scheduleId:正式診次 ID。visitReason:就診原因。不由前端傳送會員 ID、病人 ID、預約狀態或預約編號。
會員身分仍由後端依照登入 Cookie 取得。
在允許送出預約前,需要先確認:
這次也讓前端就診原因驗證與後端保持一致,包括必填、最多 500 個 Unicode code point,以及無效 Unicode 與控制字元檢查。
雖然前端會先驗證,但正式名額、重複預約及可預約時間,仍然由後端進行最終判斷。
建立預約屬於會修改資料的操作,因此需要沿用 Day 26 已建立的 Cookie Authentication 與 CSRF 保護。
送出預約前,Angular 會先取得 CSRF 驗證資訊,再透過 HttpClient 發送 POST。
如果取得 CSRF 失敗,就不會繼續建立預約。
這次沒有重新建立登入驗證機制,而是沿用既有的 AuthService。
正式預約不能只考慮成功情況,也需要處理使用者重複點擊的問題。
例如使用者按下「確認預約」後,因為網路比較慢,可能以為沒有反應而再次點擊。
因此,這次加入不同的提交狀態:
| 狀態 | 用途 |
|---|---|
idle |
尚未送出 |
submitting |
正在建立預約 |
verificationRequired |
建立結果不明,需要查核 |
succeeded |
已確認建立成功 |
failed |
已確認建立失敗 |
送出期間會停用重複提交。
如果已經確認建立成功,也不能再使用相同操作重複建立預約。
這次另一個重要的問題是:如果 POST 發生逾時或網路中斷,不能直接認為 Oracle 沒有建立預約。
因為可能是後端已經寫入成功,但前端沒有收到回應。
因此,這次在結果不明時,不會自動重送 POST,而是進入 verificationRequired 狀態。
使用者可以透過「我的預約」查詢是否已有相同診次的有效預約。
即使暫時查不到,也不代表可以立即重新送出,避免造成重複操作。
這次也針對不同錯誤提供相對應的提示。
例如:
| 錯誤 | 處理方式 |
|---|---|
| 400 | 提示資料格式錯誤 |
| 401 | 引導重新登入 |
| 403 | 提示 CSRF 驗證失敗 |
| 404 | 提示診次不存在 |
| 409 | 處理額滿、過期、未開放或重複預約 |
| 503/網路錯誤 | 停止重送,先進行查核 |
這些處理讓前端不只是可以送出預約,也能在失敗或不確定的情況下提供較安全的操作流程。
原本的預約成功頁使用 BookingDemoService 保存一次性摘要。
因此,重新整理或直接輸入網址後,可能無法再次取得預約資料。
這種方式適合 Demo,但不適合正式預約系統。
這次新增:
src/app/pages/
├── reservation-success.ts
├── reservation-success.html
└── reservation-success.css
正式路由:
/reservation-success/:appointmentId
成功建立預約後,使用後端回傳的正式 ID 進入結果頁。
結果頁會透過:
GET /api/appointments/{id}
重新取得預約詳細資料。
因此,即使重新整理頁面,也不需要再次建立預約。
正式結果頁顯示:
另外,也提供「查看預約詳細」與「我的預約」兩個入口。
如果預約狀態為 confirmed,會顯示「預約成功」及「已預約」。
如果預約已取消,則顯示「此筆預約已取消」,不會因為網址包含 success 就一直宣稱預約有效。
正式預約成功頁面

在進行 Chrome 手動驗收時,我發現會員登入後雖然可以進入個人資料頁,但原本沒有明確的「我的預約」導覽按鈕。
使用者必須直接輸入 /appointments 網址,才能進入預約列表。
雖然預約 API 本身正常,但這樣的操作流程並不直覺。
因此,這次也針對正式個人資料頁進行 UI 調整。
這次保留原本個人資料頁的設計,包括:
主要修改是將「查看我的預約」按鈕放進個人資料卡片內,位於「編輯資料」按鈕上方。
按鈕使用深藍色背景、白色文字與日曆圖示,讓使用者能清楚區分查看預約與編輯資料兩種操作。
另外,也調整按鈕間距、Hover、Focus 與 RWD。
這次 UI 調整只修改:
member.html
member.css
member.spec.ts
沒有修改 member.ts、Profile API 或原本的資料驗證邏輯。
「查看我的預約」使用 Angular RouterLink:
<a routerLink="/appointments">
查看我的預約
</a>
登出功能則繼續保留在原本的 Header 會員選單,不另外增加重複的登出按鈕。
完成修改後,也確認在個人資料編輯模式中,點擊預約連結不會意外提交表單。
修改後的正式個人資料頁,新增查看我的預約按鈕

完成正式預約流程與 UI 修正後,重新執行 Angular 自動化測試。
這次測試涵蓋:
| 項目 | 結果 |
|---|---|
| Angular 單元測試 | 422/422 通過 |
| TypeScript App | 通過 |
| TypeScript Spec | 通過 |
| Production Build | 成功,無建置警告或錯誤 |
| Profile Mock axe | 查看、編輯狀態通過 |
先前進行的 Mock axe 測試也涵蓋登入、註冊、醫師詳細、預約列表、詳細與正式預約結果頁等情境。
不過,Mock axe 使用 jsdom,並未完成色彩對比檢查,因此不能直接取代實際瀏覽器的視覺驗收。
Angular 422/422 測試結果

完成自動化測試後,我再使用 Chrome 進行實際操作。
這次不只是測試單一頁面能否顯示,也確認各頁面之間的連結與登入狀態。
主要驗收內容包括:
這次也透過實際操作發現了個人資料頁缺少預約入口的問題,進一步完成 UI 修正。
前面的 Angular Mock 測試雖然已經通過,但我仍希望確認會員能否真的透過網站建立預約,並將資料儲存到 Oracle。
因此,完成瀏覽器唯讀驗收後,我規劃了一次受控的真實掛號測試。
這次使用既有的虛構測試會員與診次。
| 項目 | 測試資料 |
|---|---|
| 測試會員 | syy001 |
| USER_ID | 7 |
| PATIENT_ID | 5 |
| 醫師 ID | 1 |
| 診次 ID | 2 |
| 預約日期 | 2026/11/02 |
| 門診時間 | 14:00~17:00 |
| 測試 RunId | D29B20261101-01 |
在執行前,先透過正式 API 及 Oracle SELECT 確認:
另外,這次只核准建立一筆預約,不執行取消、刪除或重新建立。
完成預檢後,我在 Chrome 使用 syy001 登入。
接著依照正式流程操作:
正式醫師詳細頁
↓
選擇立即預約
↓
正式預約掛號頁
↓
選擇 2026/11/02 診次
↓
填寫虛構就診原因
↓
確認預約
↓
查看正式預約結果
這次只執行一次確認預約,沒有重複送出。
完成後,取得正式預約 ID:
4
預約詳細頁網址:
http://localhost:4200/appointments/4
當時沒有保存 POST 的 Network 狀態碼,因此不能單憑畫面跳轉,就將本次 HTTP 201 記錄為已驗證。
後續改由 Oracle SELECT 與正式 API 查詢,確認實際寫入結果。
Codex 在 2026/11/01 16:57 進行建立後唯讀查核,確認資料符合單筆測試預期。
| 項目 | 建立前 | 建立後 |
|---|---|---|
| APPOINTMENTS 總數 | 2 | 3 |
| confirmed | 0 | 1 |
| cancelled | 2 | 2 |
| 診次 2 剩餘名額 | 2 | 1 |
| syy001 同診次有效預約 | 0 | 1 |
本次新增的預約資料:
| 欄位 | 實際結果 |
|---|---|
| APPOINTMENT_ID | 4 |
| PATIENT_ID | 5 |
| SCHEDULE_ID | 2 |
| DOCTOR_ID | 1 |
| REFERENCE_CODE | SC-A000000000004 |
| APPOINTMENT_STATUS | confirmed |
| CREATED_AT | 2026/11/01 16:51:46.186 +08:00 |
| CANCELLED_AT | NULL |
另外也確認:
cancelled。這次沒有再次建立或取消任何預約。
Oracle 預約 ID 4 與建立前後資料比對

完成 Oracle 查核後,我再使用 Chrome 確認:
/reservation-success/4
結果頁顯示正式預約編號:
SC-A000000000004
並顯示預約狀態為「已預約」。
重新整理頁面後,仍然可以取得相同的預約資料,不需要再次發送建立預約的 POST。
接著進入:
/appointments
確認我的預約列表包含:
最後,再查看:
/appointments/4
確認預約詳細頁的醫師、科別、日期、時段與預約編號都和結果頁一致。
因此,這次已經完成從 Angular 預約操作,到 Oracle 資料保存,再回到 Angular 查詢的完整流程驗證。
我的預約列表

為了避免覆寫 Day 27 已完成的測試紀錄,這次另外建立 Day 29 的驗收檔案:
docs/day29-D29B20261101-01-acceptance.json
紀錄內容包含測試識別碼、建立前後資料、預約 ID、診次、狀態及資料一致性檢查結果。
沒有保存會員密碼、Cookie、CSRF Token 或完整就診原因。
這次也保留了 Day 27 原本的測試紀錄,不重新執行舊測試,也不刪除已取消的預約歷史。
原本預約頁面雖然已完成 HTML、CSS 與 Angular Component,但仍使用固定 Demo 資料。
直到正式串接日期化診次與預約 API,會員才能真正透過網站建立掛號紀錄。
這次讓我更理解,前端畫面完成與前後端功能整合是不同的開發階段。
Demo 醫師 ID 與正式 Oracle 醫師 ID 並不是同一套資料。
因此,這次選擇保留 Demo 路由,另外建立正式預約頁,避免兩種資料來源混淆。
使用者可能因為等待時間較長而重複點擊,或在網路中斷時不確定預約是否成功。
因此,不能只處理一般成功情況,也需要提交中鎖定、錯誤提示及結果不明查核。
原本 Demo 成功頁依賴一次性記憶體資料,重新整理後可能無法顯示。
這次改成使用正式預約 ID 重新呼叫 API,即使重新整理,也可以再次取得資料庫中的預約紀錄。
這次在實際操作時,發現個人資料頁缺少「我的預約」入口。
雖然 API 沒有問題,但使用者不容易找到功能,因此還是需要修改導覽與畫面。
這讓我了解到,整合測試除了檢查資料與 HTTP,也需要確認使用者能否順利完成操作。
Mock 測試可以驗證程式邏輯,但不能完全取代真實 Oracle 寫入。
這次透過建立前後的 SELECT 比對,確認只新增一筆預約,並驗證名額、狀態與歷史資料一致。
同時,也學到測試時應區分「已確認的結果」與「沒有保存證據的項目」。
例如這次可以確認 Oracle 真的新增預約,但因為沒有保存建立當下的 HTTP 狀態,不能直接宣稱已驗證 HTTP 201。
經過 Day 29 的整合測試,智慧醫院管理平台第一階段的主要預約流程,已經能從前端操作一路連接到後端 API 與 Oracle 資料庫。
目前已完成的核心流程包括:
醫師查詢
↓
醫師詳細
↓
會員登入
↓
正式預約掛號
↓
預約成功
↓
我的預約
↓
預約詳細
接下來可以針對整體網站進行進一步整理,包括:
這次最大的收穫是,不再只是分別完成 Angular 畫面與 ASP.NET Core API,而是真正將前端、後端與 Oracle 串成一個可以操作的預約掛號流程,並透過自動化測試、瀏覽器驗收及真實資料庫查核,確認系統能正確保存與呈現預約紀錄。